﻿using System;
using System.ComponentModel.Composition;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Language.StandardClassification;
using System.Linq;
using System.Collections.Generic;
using Microsoft.VisualStudio.Shell.Interop;


namespace PK2
{
    //Anchors the TextViewListener to VS
    [Export(typeof(IVsTextViewCreationListener))]
    [Name("token completion handler")]
    [ContentType("Code")]
    [TextViewRole(PredefinedTextViewRoles.Editable)]
    internal class PKProvider : IVsTextViewCreationListener
    {
        [Import]
        internal IVsEditorAdaptersFactoryService AdapterService = null;
        [Import]
        internal ICompletionBroker CompletionBroker { get; set; }
        [Import]
        internal SVsServiceProvider ServiceProvider { get; set; }
        [Import]
        internal IClassifierAggregatorService AggregatorService { get; set; }
        [Import]
        internal IIntellisenseSessionStackMapService IntellisenseProvider { get; set; }
        [Import]
        internal IClassificationTypeRegistryService ClassificationRegistry { get; set; }

        internal IVsStatusbar StatusBar { get; set; }

        public void VsTextViewCreated(IVsTextView textViewAdapter)
        {
            ITextView textView = AdapterService.GetWpfTextView(textViewAdapter);
            StatusBar = ServiceProvider.GetService(typeof(IVsStatusbar)) as IVsStatusbar;
            if (textView == null)
                return;

            Func<PKCommandHandler> createCommandHandler = delegate() 
                { 
                    return new PKCommandHandler(textViewAdapter, textView, this); 
                };

            textView.Properties.GetOrCreateSingletonProperty(createCommandHandler);
        }
    }


    internal class PKCommandHandler : IOleCommandTarget
    {
        internal IOleCommandTarget m_nextCommandHandler;
        internal ITextView m_textView;
        internal PKProvider m_provider;
        internal ICompletionSession m_session;
        internal bool prepareForCompletion;
        

        internal PKCommandHandler(IVsTextView textViewAdapter, ITextView textView, PKProvider provider)
        {
            this.m_textView = textView;
            this.m_provider = provider;
            PKContext.Current.Controller.StatusBar = provider.StatusBar;
            PKContext.Current.Controller.Initialize();
            

            //add the command to the command chain
            textViewAdapter.AddCommandFilter(this, out m_nextCommandHandler);
        }

        public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
        {
            return m_nextCommandHandler.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
        }

        public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
        {
            if (VsShellUtilities.IsInAutomationFunction(m_provider.ServiceProvider))
            {
                return m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
            }

            var currentSpan = Helper.IsInComment(m_textView, m_provider.AggregatorService);

            //if we are writting outside a comment, avoid the PK 
            if (!currentSpan.Item1)
            {
                return m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
            }

            //make a copy of this so we can look at it after forwarding some commands
            uint commandID = nCmdID;
            char typedChar = char.MinValue;

            //make sure the input is a char before getting it
            if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR)
            {
                typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
            }

            //check for a commit character
            if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN
                || nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB
                || (char.IsWhiteSpace(typedChar) || char.IsPunctuation(typedChar)))
            {
                //check for a a selection
                if (m_session != null && !m_session.IsDismissed)
                {
                    //if the selection is fully selected, commit the current session
                    if (m_session.SelectedCompletionSet.SelectionStatus.IsSelected)
                    {
                        m_session.Commit();
                        //also, don't add the character to the buffer
                        return VSConstants.S_OK;
                    }
                    else
                    {
                        //if there is no selection, dismiss the session
                        m_session.Dismiss();
                    }
                }
                else if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN)
                {
                    //In here we find the provider and execute the command
                    try
                    {
                        PKCommandEventArgs e = new PKCommandEventArgs();

                        var splittedLine = Helper.SplitLine(currentSpan.Item2);

                        var provider = (from p in PKContext.Current.Controller.Plugins
                                        where p.Token == splittedLine.Token
                                        select p).FirstOrDefault();

                        if (provider == null)
                            throw new Exception("Invalid token");

                        var command = (from c in provider.Commands
                                       where c.DisplayText == splittedLine.Command
                                       select c).FirstOrDefault() ?? provider.Commands.First();

                        e.Arguments = splittedLine.Arguments;
                        e.Command = command;
                        e.CompleteLine = currentSpan.Item2.GetText();
                        e.TextView = m_textView;

                        provider.CommandExecuted(e);

                    }
                    catch (Exception ex)
                    {
                        PKContext.Current.Controller.SetTextInTheStatusBar(ex.Message);
                    }
                }
            }

            //pass along the command so the char is added to the buffer
            int retVal = m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);


            bool handled = false;
            var isLaunchToken = PKContext.Current.Controller.IsCharLaunchToken(typedChar);
            if (!typedChar.Equals(char.MinValue) && char.IsLetterOrDigit(typedChar) || isLaunchToken ) 
            {
               
                    if ((m_session == null || m_session.IsDismissed) &&  isLaunchToken) // If there is no active session, bring up completion
                    {
                        //m_textView.Caret.MoveToNextCaretPosition();
                        PKContext.Current.Controller.SetCompletionSetForChar(typedChar);
                        this.TriggerCompletion();
                        //m_session.Filter();
                    }
                    else   //the completion session is already active, so just filter
                    {
                        if (m_session != null)
                            Filter();
                    }
                    handled = true;
                
            }
            else if (commandID == (uint)VSConstants.VSStd2KCmdID.BACKSPACE   //redo the filter if there is a deletion
                || commandID == (uint)VSConstants.VSStd2KCmdID.DELETE)
            {
                if (m_session != null && !m_session.IsDismissed)
                    Filter();
                handled = true;
            }
            if (handled) return VSConstants.S_OK;
            return retVal;
        }

        private bool TriggerCompletion()
        {
            //the caret must be in a non-projection location 
            SnapshotPoint? caretPoint = m_textView.Caret.Position.Point.GetPoint(textBuffer => 
                { 
                    return (!textBuffer.ContentType.IsOfType("comment")); 
                }, PositionAffinity.Successor);

            if (!caretPoint.HasValue)
            {
                return false;
            }

            m_session = m_provider.CompletionBroker.CreateCompletionSession(m_textView,
                caretPoint.Value.Snapshot.CreateTrackingPoint(caretPoint.Value, PointTrackingMode.Positive),
                true);

            //subscribe to the Dismissed event on the session 
            m_session.Dismissed += this.OnSessionDismissed;
            m_session.Committed += this.OnSessionCommitted;
            m_session.Start();

            return true;
        }

        private void Filter()
        {
            if (m_session == null)
            {
                return;
            }
            m_session.SelectedCompletionSet.Filter();
            m_session.SelectedCompletionSet.SelectBestMatch();
            m_session.SelectedCompletionSet.Recalculate();
        }

        private void OnSessionCommitted(object sender, EventArgs e)
        {
            SpecialCompletionSet cSet = m_session.SelectedCompletionSet as SpecialCompletionSet;
            if (cSet == null) return;

            switch (cSet.LaunchToken)
            {
                case PKTokenConstants.InitToken:
                    //var text = cSet.SelectionStatus.Completion.
                    break;
                default:

                    break;
            }
        }

        private void OnSessionDismissed(object sender, EventArgs e)
        {
            m_session.Dismissed -= this.OnSessionDismissed;
            m_session.Committed -= this.OnSessionCommitted;
            m_session = null;
        }
    }
}
